using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using Nullables;
using Sedna.Domain.Enum;

namespace Sedna.Util
{
    public static class DateUtility
    {
        private static readonly DateTime MIN_DB_DATE = new DateTime(1901, 1, 1);
        private static readonly DateTime MAX_DB_DATE = new DateTime(9999, 12, 31);

        /// <summary>
        /// Truncates date not to cause overflow.
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        public static DateTime TruncDateForDb(DateTime date)
        {
            if (date < MIN_DB_DATE)
            {
                return MIN_DB_DATE;
            }   
            
            if (date > MAX_DB_DATE)
            {
                return MAX_DB_DATE;
            }
            
            return date;
        }

        /// <summary>
        /// Truncates date not to cause overflow.
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        public static NullableDateTime TruncDateForDb(NullableDateTime date)
        {
            if (date == null)
            {
                return date;
            }
            
            return TruncDateForDb(date.Value);
        }
        
        
        public static bool IsInDayRange(DateTime day, NullableDateTime dayFrom, NullableDateTime dayTo)
        {
            if (dayFrom!=null&&CompareToWithinADay(dayFrom.Value, day)>0)
            {
                return false;
            }   
            if (dayTo!=null&&CompareToWithinADay(day, dayTo.Value)>0)
            {
                return false;
            }
            return true;
        }
        
        public static int DaysBetween(DateTime startDate, DateTime endDate)
        {
            return endDate.Subtract(startDate).Add(-endDate.TimeOfDay).Add(startDate.TimeOfDay).Days;
        }
        
        public static DateTime GetDayStart(DateTime date)
        {
            return date.Add(-date.TimeOfDay);
        }
        
        public static DateTime GetDayEnd(DateTime date)
        {
            return date.Add(new TimeSpan(TimeSpan.TicksPerDay - 1) - date.TimeOfDay);   
        }
        
        public static int CompareTime(DateTime date1, DateTime date2)
        {
            return GetTime(date1).CompareTo(GetTime(date2));
        }

        public static DateTime GetTime(DateTime date)
        {
            return new DateTime(1900, 1, 1, date.Hour, date.Minute, date.Second, date.Millisecond);
        }
        
        public static int CompareToWithinADay(DateTime dt1, DateTime dt2)
        {
            return DateTime.Compare(DateTime.Parse(dt1.ToLongDateString()), DateTime.Parse(dt2.ToLongDateString()));
        }

        public static bool IsEqualToWithinADay(DateTime dt1, DateTime dt2)
        {
            return (dt1.ToLongDateString().Equals(dt2.ToLongDateString()));
        }

        public static bool IsEqualToWithinADay(NullableDateTime dt1, NullableDateTime dt2)
        {
            return IsEqualToWithinADay(dt1, dt2, false);
        }

        public static bool IsEqualToWithinADay(NullableDateTime dt1, NullableDateTime dt2, bool RiseExceptionOnAnyNull)
        {
            if (dt1 == null)
            {
                if (!RiseExceptionOnAnyNull)
                {
                    return (dt2 == null);
                }
                else
                    throw new ArgumentNullException("Argument 'dt1' is null.");
            }
            else if (dt2 == null)
            {
                if (!RiseExceptionOnAnyNull)
                {
                    return false;
                }
                else
                    throw new ArgumentNullException("Argument 'dt2' is null.");
            }

            return IsEqualToWithinADay(dt1.Value, dt2.Value);
        }
        
        public static bool IsDate(String value)
        {
            if (value.Trim() == null || value.Trim() == String.Empty)
                return false;
            DateTime someResult;
            return DateTime.TryParse(value.Trim(), Thread.CurrentThread.CurrentCulture.DateTimeFormat, DateTimeStyles.None,
                              out someResult);
        }
        
        public static int Quarter(DateTime dt)
        {
            return (dt.Month - 1)/3 + 1;
        }
        
        /// <summary>
        /// Uses:
        /// <see cref="GregorianCalendar" />
        /// <see cref="GregorianCalendarTypes.Localized" />
        /// <see cref="CalendarWeekRule.FirstDay" />
        /// <see cref="DayOfWeek.Monday" />
        /// </summary>
        public static int WeekOfYear(DateTime dt)
        {
            DayOfWeek firstDayOfYear = new DateTime(dt.Year, 1, 1).DayOfWeek;

            int shift = (int)firstDayOfYear;
            
            if (shift == 0)
                shift = 7; // Sunday

            shift--;
            
            // CultureInfo.CurrentUICulture.Calendar.GetWeekOfYear(dt, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday)

            return (dt.DayOfYear + shift - 1)/7 + 1;
        }

        /// <summary>
        /// Uses:
        /// <see cref="CalendarWeekRule.FirstDay" />
        /// <see cref="DayOfWeek.Monday" />
        /// </summary>
        public static int WeekOfMonth(DateTime dt)
        {
            DayOfWeek firstDayOfMonth = new DateTime(dt.Year, dt.Month, 1).DayOfWeek;

            int shift = (int)firstDayOfMonth;

            if (shift == 0)
                shift = 7; // Sunday

            shift--;

            return (dt.Day + shift - 1) / 7 + 1;
        }
        
        public static int FirstMonthOfQuarter(int quarter)
        {
            if (quarter < 1 || quarter > 4)
            {
                throw new ArgumentOutOfRangeException(
                    String.Format("Quarter must be between 1 and 4. Now it's {0}", quarter));
            }

            return 3 * quarter - 2;
        }
        
        /// <summary>
        /// Uses:
        /// <see cref="DayOfWeek.Monday" />
        /// </summary>
        public static DateTime FirstDateOfWeek(DateTime dt)
        {
            int dayOfWeek = (int)dt.DayOfWeek;

            if (dayOfWeek == 0)
                dayOfWeek = 7; // correction on Sunday

            dayOfWeek--;

            return dt.AddDays(-dayOfWeek).Date;
        }
        
        public static DateTime Add(DateTime dt, int value, DateTimePrecision precision)
        {
            if (value == 0)
                return dt;

            switch (precision)
            {
                case DateTimePrecision.ToYear:
                    {
                        return dt.AddYears(value);
                    }
                case DateTimePrecision.ToQuarter:
                    {
                        return Add(dt, 3, DateTimePrecision.ToMonth);
                    }
                case DateTimePrecision.ToMonth:
                    {
                        return dt.AddMonths(value);                        
                    }
                case DateTimePrecision.ToWeek:
                    {
                        return Add(dt, 7, DateTimePrecision.ToDay);
                    }
                case DateTimePrecision.ToDay:
                    {
                        return dt.AddDays(value);                        
                    }
                case DateTimePrecision.ToHour:
                    {
                        return dt.AddHours(value);                        
                    }
                case DateTimePrecision.ToMinute:
                    {
                        return dt.AddMinutes(value);                        
                    }
                case DateTimePrecision.ToSecond:
                    {
                        return dt.AddSeconds(value);                        
                    }
                case DateTimePrecision.ToMillisecond:
                    {
                        return dt.AddMilliseconds(value);                        
                    }
            }

            throw new ArgumentException(String.Format("This datetime precision '{0}' is not supported.", precision));
        }
        
        public static bool IsEqual(DateTime dt1, DateTime dt2, DateTimePrecision precision)
        {
            return Compare(dt1, dt2, precision) == 0;
        }

        public static int Compare(DateTime dt1, DateTime dt2, DateTimePrecision precision)
        {
            switch (precision)
            {
                case DateTimePrecision.ToYear:
                    {
                        return CompareToYear(dt1, dt2);
                    }
                case DateTimePrecision.ToQuarter:
                    {
                        return CompareToQuarter(dt1, dt2);
                    }
                case DateTimePrecision.ToMonth:
                    {
                        return CompareToMonth(dt1, dt2);
                    }
                case DateTimePrecision.ToWeek:
                    {
                        return CompareToWeek(dt1, dt2);
                    }
                case DateTimePrecision.ToDay:
                    {
                        return CompareToDay(dt1, dt2);
                    }
                case DateTimePrecision.ToHour:
                    {
                        return CompareToHour(dt1, dt2);
                    }
                case DateTimePrecision.ToMinute:
                    {
                        return CompareToMinute(dt1, dt2);
                    }
                case DateTimePrecision.ToSecond:
                    {
                        return CompareToSecond(dt1, dt2);
                    }
                case DateTimePrecision.ToMillisecond:
                    {
                        return CompareToMillisecond(dt1, dt2);
                    }
            }

            throw new ArgumentException(String.Format("This datetime precision '{0}' is not supported.", precision));
        }
        
        public static DateTime Convert(DateTime dt, DateTimePrecision precision)
        {
            switch (precision)
            {
                case DateTimePrecision.ToYear:
                    {
                        return new DateTime(dt.Year, 1, 1);
                    }
                case DateTimePrecision.ToQuarter:
                    {
                        return new DateTime(dt.Year, FirstMonthOfQuarter(Quarter(dt)), 1);
                    }
                case DateTimePrecision.ToMonth:
                    {
                        return new DateTime(dt.Year, dt.Month, 1);
                    }
                case DateTimePrecision.ToWeek:
                    {
                        return FirstDateOfWeek(dt);
                    }
                case DateTimePrecision.ToDay:
                    {
                        return new DateTime(dt.Year, dt.Month, dt.Day);
                    }
                case DateTimePrecision.ToHour:
                    {
                        return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0 , 0);
                    }
                case DateTimePrecision.ToMinute:
                    {
                        return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0);
                    }
                case DateTimePrecision.ToSecond:
                    {
                        return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
                    }
                case DateTimePrecision.ToMillisecond:
                    {
                        return dt;
                    }
            }            

            throw new ArgumentException(String.Format("This datetime precision '{0}' is not supported.", precision));
        }
        
        public static int CompareToYear(DateTime dt1, DateTime dt2)
        {            
            return dt1.Year.CompareTo(dt2.Year);
        }

        public static int CompareToQuarter(DateTime dt1, DateTime dt2)
        {
            int toYear = CompareToYear(dt1, dt2);

            return (toYear == 0) ? Quarter(dt1).CompareTo(Quarter(dt2)) : toYear;
        }

        public static int CompareToMonth(DateTime dt1, DateTime dt2)
        {
            int toYear = CompareToYear(dt1, dt2);

            return (toYear == 0) ? dt1.Month.CompareTo(dt2.Month) : toYear;
        }
        
        /// <summary>
        /// Uses:
        /// <see cref="GregorianCalendar" />
        /// <see cref="GregorianCalendarTypes.Localized" />
        /// <see cref="CalendarWeekRule.FirstDay" />
        /// <see cref="DayOfWeek.Monday" />
        /// </summary>
        public static int CompareToWeek(DateTime dt1, DateTime dt2)
        {
            int yearDelta = dt2.Year - dt1.Year;
            
            if (Math.Abs(yearDelta) > 1 || yearDelta < 0)
            {
                return (yearDelta > 0) ? -1 : 1;
            }
            
            if (yearDelta == 0)
            {
                // the same year

                return WeekOfYear(dt1).CompareTo(WeekOfYear(dt2));
            }
            
            // two near by years in chronological order
            
            if (dt1.Month < 12 || dt2.Month > 1)
            {                
                return -1;
            }
            
            // two near by months (December/January)
            
            if (dt1.Day <= 25 || dt2.Day >= 7)
            {
                // Even if 25 Dec is a Monday or 7 Jan is a Sunday,
                // dates belong to different weeks
                
                return -1;
            }
            
            // two near by weeks

            int dayOfWeek = (int)dt2.DayOfWeek;

            if (dayOfWeek == 0)
                dayOfWeek = 7; // correction on Sunday
            
            int weekBeginning = dt2.Day - dayOfWeek;
                        
            if (weekBeginning >= 0)
            {
                // dt2 belongs to the second week of year
                // or 1st Jan is a Monday
                
                return -1;
            }

            weekBeginning = 31 + weekBeginning + 1/*correction*/;

            return (dt1.Day < weekBeginning) ? -1 : 0;
        }

        public static int CompareToDay(DateTime dt1, DateTime dt2)
        {
            int toMonth = CompareToMonth(dt1, dt2);

            return (toMonth == 0) ? dt1.Day.CompareTo(dt2.Day) : toMonth;
        }

        public static int CompareToHour(DateTime dt1, DateTime dt2)
        {
            int toDay = CompareToDay(dt1, dt2);

            return (toDay == 0) ? dt1.Hour.CompareTo(dt2.Hour) : toDay;
        }

        public static int CompareToMinute(DateTime dt1, DateTime dt2)
        {
            int toHour = CompareToHour(dt1, dt2);

            return (toHour == 0) ? dt1.Minute.CompareTo(dt2.Minute) : toHour;
        }

        public static int CompareToSecond(DateTime dt1, DateTime dt2)
        {
            int toMinute = CompareToMinute(dt1, dt2);

            return (toMinute == 0) ? dt1.Second.CompareTo(dt2.Second) : toMinute;
        }

        public static int CompareToMillisecond(DateTime dt1, DateTime dt2)
        {
            int toSecond = CompareToSecond(dt1, dt2);

            return (toSecond == 0) ? dt1.Millisecond.CompareTo(dt2.Millisecond) : toSecond;
        }
    }
}
